
import java.io.IOException;
import java.lang.reflect.Array;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.SocketException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class Router implements Runnable {

	public static final long SLEEP_TIME = 2500;
	public static final long TIMEOUT = 8 * SLEEP_TIME;
	public static final int INFINITY= 2048;
	public static final InetSocketAddress NOT_NEIGHBOR = null;
	public static final InetSocketAddress LOCALHOST = null;
	public static final int NOT_NEIGHBOR_COST = INFINITY;
	public static final int LOCALHOST_COST = 0;
	public static final byte NO_ROUTE_TO_HOST = -1;
	


	private int[ ] initialEnlaceState;
	private byte[ ] initialPiVector;

	private int[ ] distanceVector;
	private byte[ ] piVector;
	private long[ ] lastContact;
	private InetSocketAddress[ ] neighbors;

	private byte id;
	private DatagramSocket udpSocket;
	private String ip;
	private int port;

	private boolean power;
	private boolean startTimer;
	

	public Router ( byte routerId , int UDPport , String myip ) throws SocketException {
		setId(routerId);
		setIp(myip);
		setPort(UDPport);
		setStartTimer(true);
		
	}

	public void setInitialEnlaceState ( int[ ] initialDistanceVector, InetSocketAddress[] neighbors )
		throws SocketException {
		
		
		assert initialDistanceVector.length == neighbors.length;
		assert neighbors.length < Byte.MAX_VALUE;
	
		this.distanceVector = initialDistanceVector.clone();
		this.neighbors = neighbors.clone();
		this.piVector = new byte[initialDistanceVector.length];
		for ( int i = 0; i < piVector.length; i++ ) {
			if( i == this.getId() || neighbors[i] != NOT_NEIGHBOR )
				piVector[i] = (byte) i;
			else
				piVector[i] = NO_ROUTE_TO_HOST;
		}
		
		this.initialEnlaceState = initialDistanceVector.clone();
		this.initialPiVector = piVector.clone(); 
		
		long now = System.currentTimeMillis();
		this.lastContact = new long[this.neighbors.length];
		for ( byte i = 0; i < lastContact.length; i++ ) {
			lastContact[i] = now;
		}
	}
	


	protected boolean update( RouterProtocolPacket routerPacket ) { //TODO add new neighbor???

		boolean modificationOccurred = false;
		int[ ] neighborDistanceVector = routerPacket.getDistanceVector();
		byte idNeighbor = routerPacket.getIdSender();
		
		//System.out.println("router " + getId() + ", recive packet : " + routerPacket );

		this.lastContact[idNeighbor] = System.currentTimeMillis(); 			// update neighbor timeout timer
		if( this.initialEnlaceState[idNeighbor] < this.distanceVector[idNeighbor]) {			// he came back from the dead!!!!
			this.distanceVector[idNeighbor] = this.initialEnlaceState[idNeighbor];
			this.piVector[idNeighbor] = this.initialPiVector[idNeighbor];
		}

		
		for ( int i = 0; i < neighborDistanceVector.length; i++ ) { 		// update distance vector
			if( i == this.getId() )
				continue;
			if( neighborDistanceVector[i] >= INFINITY || this.distanceVector[idNeighbor] >= INFINITY )
				continue;
			else if ( this.distanceVector[idNeighbor] + neighborDistanceVector[i] < this.distanceVector[i] ) {	// if neccessary
				this.distanceVector[i] = (byte) (neighborDistanceVector[i] + this.distanceVector[idNeighbor]);
				this.piVector[i] = idNeighbor;
				modificationOccurred = true;
			}
		}
		
		return modificationOccurred;
	}


	public void timeEventOccurred() {

//		System.out.println(" hahahah");
		try {
			this.verifyNeighboursTimeout();
			this.notifyNeighbors();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	private void verifyNeighboursTimeout() { //TODO remove from neighbours/ NO_ROUTE_TO_HOST
		long now = System.currentTimeMillis();
		for (int neighbourID = 0; neighbourID < this.getNeighbors().length; neighbourID++) {
			InetSocketAddress neighborAddress = this.getNeighbors()[neighbourID];
			
			if( neighbourID == this.getId() )
				continue;
			if( neighborAddress != NOT_NEIGHBOR  ) { // is a neighbour
				if( now - this.lastContact[neighbourID] > TIMEOUT ){
					for ( int i = 0; i < this.piVector.length; i++ ) {
						if( this.piVector[i] == neighbourID  && i != neighbourID){
							this.distanceVector[i] = this.initialEnlaceState[i];
							this.piVector[i] = this.initialPiVector[i];
						}else{
							this.distanceVector[neighbourID] = INFINITY;
							this.piVector[neighbourID] = NO_ROUTE_TO_HOST;							
						}
					}

				}
			}
		}
	}

	public void run() {

		try {
			
			this.power = true;
			this.setUdpSocket(new DatagramSocket( this.getPort() ));
			if( this.startTimer ) // used in testing
				new Timer( this, SLEEP_TIME ).start();
			this.initializeData();
			this.notifyNeighbors();
			while ( this.isPowerOn() ) {
				RouterProtocolPacket routerPacket = RouterProtocol.receivePacket( this.getUdpSocket() );
				boolean modificationOccurred = this.update( routerPacket );
				if ( modificationOccurred ){
					notifyNeighbors();
				}
			}
			
		} catch ( Exception e ) {
			System.err.println( "Router"+this.getId() +" is going down. Reason: " +e.getClass().getSimpleName() );
			return;
		}
		System.err.println( "Router"+this.getId() +" is going down. Reason: power off");
	}

	private void initializeData() {

		this.distanceVector = this.initialEnlaceState.clone();
		this.piVector = this.initialPiVector.clone(); 
		long now = System.currentTimeMillis();
		this.lastContact = new long[this.neighbors.length];
		for ( byte i = 0; i < lastContact.length; i++ ) {
			lastContact[i] = now;
		}
	}

	private void notifyNeighbors() throws IOException {
		RouterProtocolPacket packet = new RouterProtocolPacket( this.getId(), this.getDistanceVector() );
		for (InetSocketAddress neighborAddress : this.getNeighbors()) {
			if( neighborAddress != NOT_NEIGHBOR && neighborAddress != LOCALHOST ){				
				RouterProtocol.sendRouterProtocolPacket(packet, this.getUdpSocket(), neighborAddress );				
			}
		}
		//System.out.println("Router " + this.getId() + " sent a packet ( "+ packet +" ) to neighbors");
	}


	protected byte getId() {

		return id;
	}

	protected void setId( byte id ) {

		this.id = id;
	}


	protected int[ ] getDistanceVector() {

		return distanceVector;
	}

	protected void setDistanceVector( int[ ] distanceVector ) {

		this.distanceVector = distanceVector;
	}

	protected byte[ ] getPIVector() {

		return piVector;
	}

	protected void setPIVector( byte[ ] piVector ) {

		this.piVector = piVector;
	}

	protected DatagramSocket getUdpSocket() {

		return udpSocket;
	}

	protected void setUdpSocket( DatagramSocket udpSocket ) {

		this.udpSocket = udpSocket;
	}

	protected InetSocketAddress[ ] getNeighbors() {

		return neighbors;
	}

	protected void setNeighbors( InetSocketAddress[ ] neighbors ) {

		this.neighbors = neighbors;
	}
	
	public String toString(){
		return " RouterID = "+this.getId() +" Distance Vector = "+ Arrays.toString( this.distanceVector) +" Predecessor Vector = "+ Arrays.toString( this.piVector )
				+" Neighbors = "+Arrays.toString(neighbors);
	}

	public String getIp() {
		return ip;
	}

	public void setIp(String ip) {
		this.ip = ip;
	}

	public int getPort() {
		return port;
	}

	public boolean isPowerOn() {
		return power;
	}
	
	public void shutdown(){
		this.power = false;
		if( this.udpSocket != null ){
			this.udpSocket.close();
			this.udpSocket = null;
		}
	}
	
	private void setPort(int pport) {
		this.port = pport;		
	}

	protected boolean isStartTimer() {
		return startTimer;
	}

	protected void setStartTimer(boolean startTimer) {
		this.startTimer = startTimer;
	}



	

}
